LangGraphの会話履歴をメモリ保持しよう #ai #langgraph #azure #openai #llm #python

はじめに
当ブログではここまで
にて、LangGraphを使ってAzure OpenAIとやりとりする方式を見てきました。
これらでの「やりとり」は一問一答、1回きりです。質疑応答の内容は直前であっても一切覚えていません。そのため本家 ChatGPT のように会話を続けて情報を徐々に、あるいは次々引き出したり、または方向性を軌道修正したり、ということができません。
そこで本稿では、LangGraphの機能を使ってやりとりの履歴をメモリに保持し、会話ができるようにしてみます。
会話履歴を保持できないチャットボット
次はやりとりの履歴を保持できないチャットボットの例です。
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
from langchain_openai import AzureChatOpenAI
from yaspin import yaspin
import os
from dotenv import load_dotenv
load_dotenv()
# os.getenv("AZURE_OPENAI_API_KEY")
# os.getenv("AZURE_OPENAI_ENDPOINT")
llm = AzureChatOpenAI(
azure_deployment = os.getenv("AZURE_OPENAI_MODEL_NAME"),
api_version = os.getenv("AZURE_OPENAI_API_VERSION"),
temperature = 0.95,
max_tokens = 1024,
)
class State(TypedDict):
messages: Annotated[list, add_messages]
def chatbot(state: State):
with yaspin(text="Processing", color="yellow") as spinner:
res = llm.invoke(state["messages"])
spinner.ok("✅ ")
return {"messages": res}
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot)
graph_builder.set_entry_point("chatbot")
graph_builder.set_finish_point("chatbot")
graph = graph_builder.compile()
def stream_graph_updates(user_input: str):
events = graph.stream(
{"messages": [("user", user_input)]},
stream_mode="values"
)
for event in events:
print(event["messages"][-1].content)
while True:
try:
user_input = input("User: ")
if user_input.lower() in ["quit", "exit", "q"]:
print("Goodbye!")
break
stream_graph_updates(user_input)
except Exception as e:
print(f"error: {e}")
break
これを実行すると User: と入力待ちになるので、自己紹介してみましょう。
User: 私の名前はボブです。 私の名前はボブです。 ✅ Processing こんにちはボブさん!どうぞよろしくお願いします。何かお手伝いできることがあれば、教えてください。 User:
きちんと名前を呼び返してくれました。続けて User: と入力待ちになるので、今教えた名前を聞き返してみましょう。
User: 私の名前はボブです。 私の名前はボブです。 ✅ Processing こんにちはボブさん!どうぞよろしくお願いします。何かお手伝いできることがあれば、教えてください。 User: 私の名前は何ですか? 私の名前は何ですか? ✅ Processing 私の知識ベースには、あなたの個人情報が含まれていませんので、あなたの名前が何であるかを知ることはできません。あなた自身によって提供されない限り、あなたの名前を知る方法はありません。 User:
直前に自己紹介していたにも関わらず、「知らない」となってしまいました。これはつまり、直前のやりとりを一切覚えていない、参照していないことを示しています。これではチャットボットとして使えません。
会話履歴をメモリ保持するチャットボット
LangGraphには状態を永続化する仕組みがありますので、これを利用して会話履歴を保持できます。公式ドキュメントでAdding Memory to the Chatbotとして触れられている方法です。
ここでは会話履歴をメモリに保持する形式を取るので、MemorySaverを使います。
from langgraph.checkpoint.memory import MemorySaver
グラフをコンパイルする際に、メモリに状態を保存するように指定します。LangGraphの用語で、保存する状態のスナップショットを Checkpoint と呼び、実際に保存する仕組みを Checkpointer と呼びます。
memory = MemorySaver() graph = graph_builder.compile(checkpointer=memory)
Checkpointer によって保存された各 Checkpoint には、一意のIDを割り当てます。これをLangGraphの用語で Thread と呼びます。ここでは Thread のIDを 1 として固定しています。
events = graph.stream(
{"messages": [("user", user_input)]},
{"configurable": {"thread_id": "1"}},
stream_mode="values"
)
前項の「会話履歴を保持できないチャットボット」との差分は次のようになります。
--- 23_chatbot_nomem.py 2024-11-08 18:16:32.580373788 +0900
+++ 23_chatbot_simple.py 2024-11-08 18:25:42.232364204 +0900
@@ -6,6 +6,7 @@
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
from langchain_openai import AzureChatOpenAI
+from langgraph.checkpoint.memory import MemorySaver
from yaspin import yaspin
import os
@@ -35,11 +36,13 @@
graph_builder.set_entry_point("chatbot")
graph_builder.set_finish_point("chatbot")
-graph = graph_builder.compile()
+memory = MemorySaver()
+graph = graph_builder.compile(checkpointer=memory)
def stream_graph_updates(user_input: str):
events = graph.stream(
{"messages": [("user", user_input)]},
+ {"configurable": {"thread_id": "1"}},
stream_mode="values"
)
for event in events:
全体のコードは次の通りです。
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
from langchain_openai import AzureChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from yaspin import yaspin
import os
from dotenv import load_dotenv
load_dotenv()
# os.getenv("AZURE_OPENAI_API_KEY")
# os.getenv("AZURE_OPENAI_ENDPOINT")
llm = AzureChatOpenAI(
azure_deployment = os.getenv("AZURE_OPENAI_MODEL_NAME"),
api_version = os.getenv("AZURE_OPENAI_API_VERSION"),
temperature = 0.95,
max_tokens = 1024,
)
class State(TypedDict):
messages: Annotated[list, add_messages]
def chatbot(state: State):
with yaspin(text="Processing", color="yellow") as spinner:
res = llm.invoke(state["messages"])
spinner.ok("✅ ")
return {"messages": res}
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot)
graph_builder.set_entry_point("chatbot")
graph_builder.set_finish_point("chatbot")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
def stream_graph_updates(user_input: str):
events = graph.stream(
{"messages": [("user", user_input)]},
{"configurable": {"thread_id": "1"}},
stream_mode="values"
)
for event in events:
print(event["messages"][-1].content)
while True:
try:
user_input = input("User: ")
if user_input.lower() in ["quit", "exit", "q"]:
print("Goodbye!")
break
stream_graph_updates(user_input)
except Exception as e:
print(f"error: {e}")
break
では、実際に動かしてみましょう。「会話履歴を保持できないチャットボット」と同じく、自己紹介から始めます。
User: 私の名前はボブです。 私の名前はボブです。 ✅ Processing こんにちは、ボブさん。どうぞよろしくお願いします。何かお手伝いできることがあれば、お知らせください。 User:
ここで聞き返してみましょう。
User: 私の名前はボブです。 私の名前はボブです。 ✅ Processing こんにちは、ボブさん。どうぞよろしくお願いします。何かお手伝いできることがあれば、お知らせください。 User: 私の名前は何ですか? 私の名前は何ですか? ✅ Processing あなたの名前はボブだとおっしゃいました。 User:
名前を覚えていました! さらに身長・体重を伝えてみましょう。
User: 私の名前は何ですか? 私の名前は何ですか? ✅ Processing あなたの名前はボブだとおっしゃいました。 User: 私は身長193cm、体重95kgです。 私は身長193cm、体重95kgです。 ✅ Processing 了解しました。あなたは身長193cm、体重95kgですね。何か特定の情報や質問があれば、お気軽にどうぞ。 User:
覚えてくれたようですね。プロフィールを聞いてみましょう。
User: 私は身長193cm、体重95kgです。 私は身長193cm、体重95kgです。 ✅ Processing 了解しました。あなたは身長193cm、体重95kgですね。何か特定の情報や質問があれば、お気軽にどうぞ。 User: 私のプロフィールを教えてください。 私のプロフィールを教えてください。 ✅ Processing あなたが提供した情報に基づいて、以下のようなプロフィールをまとめることができます: - 名前: ボブ - 身長: 193cm - 体重: 95kg この情報以外にも、あなたの趣味、興味、職業など、さらに詳しいプロフィールを共有したい場合は、追加情報を教えてください。プロフィールは個人をよりよく理解するための基本的なデータや興味、活動などに基づいて作成されます。 User:
直前の身長・体重だけでなく、最初に教えた名前もきちんと返してくれました。
一旦終了してみましょう。このスクリプトでは quit と入力すると終了します。
この情報以外にも、あなたの趣味、興味、職業など、さらに詳しいプロフィールを共有したい場合は、追加情報を教えてください。プロフィールは個人をよりよく理解するための基本的なデータや興味、活動などに基づいて作成されます。 User: quit Goodbye!
再度スクリプトを実行し、プロフィールを聞いてみます。
User: 私のプロフィールを教えてください。 私のプロフィールを教えてください。 ✅ Processing すみませんが、私はあなたのプロフィールに関する情報を持っていません。私はOpenAIの人工知能アシスタントで、個人情報やユーザープロフィールにアクセスする能力がありません。私の目的は、質問に答えたり、一般的な情報を提供したりすることです。プライバシーとセキュリティを保つため、このようなシステムが設計されています。何か質問があれば、それに関する情報を提供する手助けをすることはできますが、個人的な情報についてはお手伝いできません。 User:
今回はメモリに記憶しているので、スクリプトを一旦終了したことによりメモリがクリアされ、先程の会話の内容をすべて忘れてしまったことになります。
まとめ
本稿では、LangGraphの機能を使って、生成AIとのやりとりの履歴をメモリに保持し、会話がつながるようにしてみました。また一歩、実用的なチャットボットに近づいたことになります。
ただ先に述べた通り、一旦スクリプトを再起動すると会話がつながらなかったり、コンソールアプリであるため使い勝手がよくなかったりと、まだまだ実用には遠いです。
引き続き、実用的なチャットボットに向けて段階的に強化・改良を進めていきます。
